3장. 복잡함은 어디서 오나 — 부품 수가 아니라 얽힘

출처: 『소프트웨어 설계의 결합 균형』(블라드 코노노프 지음, 장연호 옮김, 제이펍 2026) | 원서: Balancing Coupling in Software Design (Manning) · 입문판·PDF 재구성

코드는 분위기만 — class·struct·Query 같은 말은 몰라도 됩니다. 표의 '비유'와 '위험'만 봐도 충분해요.


0. 이 장의 새 단어

대부분의 어려운 말은 0장 용어집에 있다.

여기엔 0장에 없는, 이 장에서 처음 나오는 말 3개만 적는다.

막히면 0장으로 돌아가면 된다.


제약 (Constraint)

한 문장 뜻 — 부품이 할 수 있는 일이나 가질 수 있는 값을 못 하게 막는 규칙.

일상비유 — 콘센트 구멍. 모양이 안 맞는 플러그는 아예 안 꽂힌다. 그래서 엉뚱한 걸 꽂아 사고 날 일이 줄어든다.

한 줄 예 —

if (a + b < c) // 조건이 참일 때만 아래 결정을 실행합니다.
{
    raise new Error("삼각형이 안 됨"); // 잘못된 값을 막는 규칙 = 제약
}

불변식 (Invariant)

한 문장 뜻 — 언제 어느 때고 항상 참이어야 하는 조건. 깨지면 그건 버그다.

일상비유 — "통장 잔액은 0원 밑으로 안 내려간다." 입금·출금이 어떻게 오가도 이 약속은 늘 지켜져야 한다.

한 줄 예 —

assert balance >= 0; // 무슨 일이 있어도 이 줄은 참이어야 함

모놀리식 덩어리 (Monolithic Blob)

한 문장 뜻 — 모든 기능을 한 곳에 욱여넣어 통째로 굴리는 거대한 한 덩어리.

일상비유 — 칸 없는 원룸에 살림을 다 쌓아 둔 것. 밖에서 보면 깔끔한 방 하나지만, 안은 발 디딜 틈이 없다.

한 줄 예 —

public class GodService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
    public void Order() { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
    public void Pay() { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
    public void Email() { /* 구현 생략 */; } // 수십 가지 일이 한 클래스에
}

이런 적 있죠?

2장에서 복잡함에도 종류가 있다고 배웠다.

규칙만 따르면 되는 것, 전문가에게 물어야 하는 것, 직접 해 봐야 아는 것.

그런데 한 가지 의문이 남는다.

복잡함은 대체 어디서 생겨나는 걸까?

부품이 많아서일까?

// 부품 5,000개짜리 시스템
var big_system = build_with(parts=5000); // 그런데 멀쩡히 잘 돈다

// 부품 5개짜리 시스템
var small_system = build_with(parts=5); // 그런데 한 군데 고치면 다 터진다

이상하다.

부품이 적은 쪽이 더 자주 터진다.

그렇다면 복잡함의 진짜 범인은 개수가 아니다.

이 장의 답은 한마디다 — 부품 사이가 어떻게 얽혔는가가 범인이다.


이 장에서 딱 5가지만

  1. 복잡함엔 두 출처가 있다. 도메인이 원래 어려운 것(필수적)과 우리가 잘못 설계해 얹은 것(우발적). 우리가 지울 수 있는 건 후자뿐이다.
  2. 복잡함은 부품 수가 아니라 상호작용에서 나온다. 영향을 따라갈 수 있으면 선형적, 못 따라가면 복잡한 상호작용이다.
  3. 복잡함은 어느 수준에서나 생긴다. 부품 사이(전역)와 부품 안(지역). 한쪽만 줄이면 다른 쪽이 폭발한다.
  4. 자유도가 높을수록 위험하다. 따로따로 정할 값이 많을수록 어긋날 자리가 많아진다.
  5. 제약이 자유도를 줄이는 도구다. 그리고 결합을 잘 설계하는 것이 곧 제약을 잘 거는 일이다.

개념 1 — 복잡함의 두 출처: 도메인 탓 vs 내 탓

망가지는 장면

"이 시스템 너무 복잡해" 한마디로 다 묶어 버렸다.

그래서 지워야 할 복잡함까지 "어쩔 수 없는 거"로 두고 넘어갔다.

반대로 어쩔 수 없는 복잡함을 억지로 지우려다 시스템이 멈췄다.

둘을 안 가른 게 화근이다.

비유 먼저

세금 계산을 떠올려 보자.

세법 자체가 복잡한 건 어쩔 수 없다. 내가 만든 게 아니라 원래 그렇다.

그런데 영수증을 일부러 흘림체로 흘려 써서 더 못 알아보게 만들면?

그건 내가 얹은 복잡함이다. 안 그래도 됐다.

비유 코드 위험
세법 자체의 복잡함(필수적) compute_tax(income, region) 못 지움 — 잘 쪼개서 관리만
흘림체 영수증(우발적) 유행 따라 쪼갠 불필요한 서비스 지울 수 있는데 안 지우면 짐

한 문장 정의 — 필수적 복잡성은 도메인 자체에 박힌 어쩔 수 없는 복잡함이라 관리만 가능하고, 우발적 복잡성은 우리가 설계를 잘못해 얹은 복잡함이라 제거 대상이다.

예시 폭격

worked(완성예) — 우발적 복잡성을 양산하는 모습:

// 유행이라는 이유로 굳이 원격 호출로 쪼갬
public class OrderService // 주문 흐름을 조정하는 서비스입니다. 어떤 협력 객체를 아는지가 결합을 만듭니다.
{
    public dynamic PlaceOrder(dynamic req) // 이 메서드의 입력과 출력이 공개 약속입니다.
    {
        var stock = http.Get($"inventory/stock/{req.Item}"); // 굳이 네트워크?
        var pay = http.Post("payment/charge", req.Card); // 같은 덩어리인데?
        // 네트워크 끊기면? 부분 실패는? 재시도는? → 안 해도 될 고생을 사서 함
}
}

부분완성(빈칸 채우기) — 둘 중 지울 수 있는 복잡함은?

var a = "금융 규제·시장 역학이 원래 복잡함"; // 필수적
var b = "필요 없는데 마이크로서비스로 쪼갬"; // 우발적
// 지울 수 있는 건 ____ 다.   (정답: b)

독립적용 — 다음을 필수적/우발적로 갈라 보자.

// "병원 예약은 의사 일정·보험·진료과가 원래 얽혀 복잡" → ____   (필수적)
// "간단한 화면 하나에 추상 계층을 5겹 쌓음" → ____            (우발적)

개념 2 — 선형적 vs 복잡한 상호작용: 따라갈 수 있나

망가지는 장면

부품이 딱 5개뿐인 작은 시스템이었다.

그런데 한 군데를 고치니 엉뚱한 데가 터지고, 그게 또 다른 데를 터뜨렸다.

"이렇게 작은데 왜 이러지?" — 크기로는 설명이 안 됐다.

비유 먼저

기계식 시계.

기어가 수십 개여도, 이 기어를 돌리면 저 기어가 도는 걸 눈으로 따라갈 수 있다.

그래서 고장 난 시계도 차근차근 고친다. 이게 선형적 상호작용이다.

반대로 연쇄 정전을 보자.

발전소 하나가 꺼지자 옆이 줄줄이 꺼졌다. 아무도 그 길을 미리 못 봤다.

이게 복잡한 상호작용이다.

비유 코드 위험
시계 기어(선형적) total = price * qty 영향이 뻔함 — 안심하고 고침
연쇄 정전(복잡) deploy() 했더니 딴 서비스가 죽음 영향 예측 불가 — 해 봐야 앎

한 문장 정의 — 한 곳을 건드렸을 때 영향을 추적할 수 있으면 선형적 상호작용이고, 영향이 엉뚱한 데로 예측 불가하게 번지면 복잡한 상호작용이다.

복잡한 상호작용은 두 얼굴로 나타난다.

  • 작동은 하는데 아무도 왜인지 모름 — 만든 사람이 떠났거나, 코드가 너무 꼬였거나.
  • 목표는 이뤘는데 사고가 남 — 멀리 떨어진 한 곳을 빠뜨려 시스템이 일관성을 잃음.

복잡한 상호작용에서 변화의 효과를 아는 유일한 방법은 실험이다. 해 보고 관찰하는 것. (2장 복잡 도메인과 그대로 이어진다.)

예시 폭격

worked — 흔한 복잡한 상호작용: 암묵적 가정이 깨지는 순간:

public class ShippingService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
    public dynamic CalcFee(dynamic order) // 이 메서드의 입력과 출력이 공개 약속입니다.
    {
        // "네트워크는 늘 멀쩡하다"는 가정이 코드 어디에도 안 적힌 채 숨어 있음
        var rates = http.Get("pricing-service/rates").Json(); // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
        return rates["standard"] * order.Weight; // 결과를 호출자에게 돌려줍니다.
        // pricing-service가 0.3초 느려지면? 끊기면? → 숨은 가정이 깨지며 사고
}
}

잘못된 예 vs 올바른 예 — 멀리 떨어진 변경:

// 잘못된 예: 필드 이름만 바꾸고 멀리 있는 쿼리는 안 고침
order.Timestamp = /* 구현 생략 */; // 이름은 바꿈
db.Execute("/* 구현 생략 */ opened_on /* 구현 생략 */"); // 옛 이름이 박힌 줄은 그대로 → 나중에 폭발

// 올바른 예: 영향 받는 모든 곳을 같이 고침
order.Timestamp = /* 구현 생략 */; // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
db.Execute("/* 구현 생략 */ timestamp /* 구현 생략 */"); // 한 번에 맞춰 일관성 유지

독립적용 — 다음은 선형적일까 복잡한 상호작용일까?

// "부품 100만 개지만 각 부품은 딱 다음 부품 하나에만 영향" → ____   (선형적)
// "서비스 10개가 서로서로 다 직접 호출" → ____                     (복잡)

개념 3 — 복잡함은 크기와 무관하다

망가지는 장면

"시스템이 너무 커서 복잡한 거야. 잘게 쪼개면 되겠지."

그래서 무작정 잘게 쪼갰다.

그런데 더 복잡해졌다.

크기를 줄이는 게 복잡함을 줄이는 게 아니었다.

비유 먼저

큰 도서관과 어질러진 방.

도서관은 책이 수십만 권이어도 분류가 명확해 원하는 책을 금방 찾는다.

어질러진 방은 물건이 몇 개 안 돼도 어디 뭐가 있는지 모른다.

개수가 아니라 정돈 상태가 머릿속 부담을 정한다.

비유 코드 위험
잘 정돈된 큰 도서관 부품 5,000개 + 선형 상호작용 커도 안 복잡 — 추적 가능
어질러진 작은 방 부품 5개 + 복잡한 상호작용 작아도 복잡 — 심하면 혼돈

한 문장 정의 — 복잡함은 부품의 수나 시스템의 크기로 정해지지 않고, 오직 부품 사이 상호작용의 특성으로 정해진다.

표면이 선형적으로만 보여도, 복잡함이 그 아래에 숨어 있을 수 있다는 점을 잊지 말자.

예시 폭격

worked — 같은 크기, 다른 복잡함:

// A: 부품 5개인데 서로 다 얽힘 → 한 곳 고치면 나머지 다 확인해야 함
var a = mesh(parts=5); // 작지만 복잡

// B: 부품 5,000개인데 한 줄로 이어짐 → 영향이 한 방향으로만 흐름
var b = pipeline(parts=5000); // 크지만 단순

부분완성 — 더 복잡한 쪽은?

var line = "역마다 정차하는 기차, 역은 수백 개"; // 영향이 순서대로 뻔함
var web = "서로 전화 돌리는 부서 10개"; // 누가 누구에 영향 주는지 꼬임
// 더 복잡한 쪽은 ____ 다.   (정답: web)

독립적용 — "부품 수를 줄이면 복잡함도 준다"는 말이 왜 틀린지 한 줄로 답해 보자.

// 답 예: 복잡함은 개수가 아니라 부품 사이 상호작용에서 나오므로, 개수만 줄여도 얽힘은 그대로다.

개념 4 — 계층적 복잡함: 지역 vs 전역

망가지는 장면

"이 시스템 복잡함은 서비스끼리 통신이 문제야."

그래서 서비스 사이만 손봤다.

그런데 막상 한 서비스를 열어 보니, 그 안이 또 엉망이었다.

복잡함은 한 층에만 있는 게 아니었다.

비유 먼저

회사 조직을 떠올리자.

부서끼리 협업이 꼬이면 부서 사이 복잡함이다.

그런데 한 부서 안을 들여다보니 팀원끼리도 일이 꼬여 있다 — 부서 안 복잡함이다.

밖에서 한 덩어리로 보이던 부서가, 안으로 들어가면 또 하나의 작은 조직이다.

비유 코드 위험
부서 사이 협업 꼬임(전역) 마이크로서비스끼리 통신 서비스 간 호출이 스파게티
부서 안 팀원 꼬임(지역) 한 서비스 안 객체끼리 한 덩어리 안이 발 디딜 틈 없음

한 문장 정의 — 전역 복잡성은 부품들 사이의 상호작용에서 나오는 복잡함이고, 지역 복잡성은 한 부품 안의 상호작용에서 나오는 복잡함이다.

여기서 한 가지가 더 중요하다.

관점을 바꾸면 둘은 서로 바뀐다.

한 부서를 통째로 위에서 보면 그건 "부품 하나"(지역).

그 부서 안으로 들어가면 그건 다시 "여러 부품의 모임"(전역).

팀 버너스리의 말처럼 "모든 시스템은 더 큰 시스템의 부품"이다. 그래서 어느 수준에서 보든 목적·부품·상호작용으로 쪼개 볼 수 있고, 복잡함은 모든 수준에서 생긴다.

핵심 — 지역·전역 중 어느 하나가 더 중요하지 않다. 둘 다 해결해야 한다.

예시 폭격

worked — 같은 시스템을 두 수준으로 보기:

// 위에서 보면: 서비스들이 서로 호출 = 전역 복잡성
var system = [case_service, billing_service, notify_service]; // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.

// case_service 안으로 들어가면: 객체들이 서로 호출 = 지역 복잡성
var case_service = [SupportCase, Message, Attachment]; // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
// 같은 복잡함인데, 보는 높이에 따라 전역도 되고 지역도 된다

미니 시나리오 — "이럴 때 이렇게":

// 상황: "서비스 간 통신만 정리하면 끝" 이라고 생각함
// 잘못된 판단: 전역만 보고 손 뗌
// 올바른 판단: 각 서비스 안(지역)도 열어 본다 — 복잡함은 두 층에 다 있다

개념 5 — 한쪽만 줄이면 다른 쪽이 폭발한다

망가지는 장면

전역 복잡함을 0으로 만들고 싶었다.

방법은 간단했다 — 모든 걸 한 덩어리로 합쳤다. 부품 사이가 없으니 부품 사이 복잡함도 없다.

그런데 그 한 덩어리 안이 지옥이 됐다.

비유 먼저

집을 생각하자.

칸막이를 다 없애고 원룸으로 만들면, 방 사이 동선 고민은 사라진다(전역 0).

대신 모든 살림이 한 칸에 쌓여 그 안이 아수라장이 된다(지역 폭발).

반대로 방을 너무 잘게 쪼개 침대 칸·베개 칸·이불 칸으로 나누면?

각 칸은 단순하지만(지역 0), 칸 사이 오가는 게 미로가 된다(전역 폭발).

비유 코드 위험
원룸으로 다 합침 모든 기능을 한 클래스에 전역 ↓, 지역 폭발
칸을 과하게 쪼갬 "서비스는 100줄 이하" 규칙 지역 ↓, 전역 폭발

한 문장 정의 — 전역 복잡성만 줄이려고 다 합치면 지역 복잡성이 폭발하고(모놀리식 덩어리), 지역 복잡성만 줄이려고 잘게 쪼개면 전역 복잡성이 폭발한다(분산된 큰 진흙덩이).

예시 폭격

worked — 전역만 줄인 함정(모놀리식 덩어리):

public class GodService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
    public void CreateOrder() { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
    public void ProcessPayment() { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
    public void SendEmail() { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
    public void UpdateStock() { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
    public void MakeReport() { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
    // 부품 사이 통신은 0. 그런데 이 클래스 안이 수천 줄 → 아무도 전체를 모름
}

잘못된 예 vs 올바른 예 — 지역만 줄인 함정(분산 큰 진흙덩이):

// 잘못된 예: 크기만 보고 잘게잘게
var services = ["주문생성", "재고확인", "가격계산", "할인적용", "세금계산"]; // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
// 각 50줄로 단순 — 그런데 서로 호출하는 그래프가 거미줄 → 전역 폭발

// 올바른 기준: 크기가 아니라 상호작용으로 자른다
// "이 둘은 자주 함께 변하나? 그럼 같은 덩어리." (크기는 기준이 아님)

미니 시나리오 — "이럴 때 이렇게":

// 상황: 모놀리식이 무겁다 → 잘게 쪼개자
// 함정: "줄 수"를 기준으로 100줄 제한을 건다 → 분산 큰 진흙덩이
// 안전판: 줄 수 말고 "함께 변하는가 / 지식을 얼마나 공유하나"로 경계를 긋는다

개념 6 — 자유도: 따로 정할 값이 많을수록 위험

망가지는 장면

같은 규칙을 두 곳에 복사해 뒀다.

"$100 넘으면 무료 배송" — 주문 쪽에도, 배송 쪽에도.

나중에 주문 쪽만 $150으로 바꿨다.

고객은 주문할 땐 무료 배송이라 들었는데, 배송 때 요금이 청구됐다.

비유 먼저

도형을 떠올리자.

정사각형은 한 변 길이 하나만 정하면 끝난다. 정할 게 1개다.

직사각형은 가로세로를 따로 정해야 한다. 정할 게 2개다.

정할 게 많을수록, 둘이 어긋날 자리도 많아진다.

비유 코드 위험
정사각형(정할 값 1개) Square(edge) 어긋날 자리 없음 — 안전
직사각형(정할 값 2개) Rectangle(width, height) 두 값이 따로 놀 수 있음

한 문장 정의 — 자유도는 서로 제약 없이 따로따로 정해야 하는 값의 개수이며, 높을수록 시스템이 가질 수 있는 상태가 늘어 어긋날 위험과 복잡함이 커진다.

소프트웨어에서 자유도가 슬며시 높아지는 두 원인이 있다.

  • 데이터 복제 — 같은 데이터를 두 DB에 두면 둘이 어긋날 수 있다.
  • 로직 중복 — 같은 규칙을 두 곳에 복사하면 둘이 어긋날 수 있다.

예시 폭격

worked — 도형으로 본 자유도:

var square = {"edge": 5}; // 자유도 1 — 이거 하나로 넓이·둘레 다 나옴
var rectangle = {"w": 5, "h": 3}; // 자유도 2 — 둘을 따로 정해야 함

잘못된 예 vs 올바른 예 — 규칙 중복:

// 잘못된 예: 같은 규칙을 두 곳에 복사 (자유도 2)
static dynamic IsFreeShippingOrder(dynamic o) // 한 줄짜리 규칙도 호출자가 알게 되는 약속입니다.
{
    return o.Amount > 100; // 계산 결과를 호출자에게 돌려줍니다.
}
static dynamic FreeShipShip(dynamic o) // 한 줄짜리 규칙도 호출자가 알게 되는 약속입니다.
{
    return o.Amount > 100; // 계산 결과를 호출자에게 돌려줍니다.
}
// 한쪽만 150으로 바꾸면? 두 답이 어긋남 → 사고

// 올바른 예: 한 곳에만 두고 둘이 갖다 씀 (자유도 1)
static dynamic FreeShip(dynamic o) // 한 줄짜리 규칙도 호출자가 알게 되는 약속입니다.
{
    return o.Amount > 100; // 계산 결과를 호출자에게 돌려줍니다.
}
// 주문도 배송도 free_ship 하나를 부른다 → 바꿔도 한 곳만

독립적용 — 다음 코드의 자유도는 몇일까?

static dynamic TaxA(decimal amount) // 한 줄짜리 규칙도 호출자가 알게 되는 약속입니다.
{
    return amount * 0.1; // 계산 결과를 호출자에게 돌려줍니다.
}
static dynamic TaxB(decimal amount) // 한 줄짜리 규칙도 호출자가 알게 되는 약속입니다.
{
    return amount * 0.1; // 계산 결과를 호출자에게 돌려줍니다.
}
// 자유도는 ____ 다. 1로 줄이려면 ____.
// (정답: 2 / 세율을 한 곳에만 두고 둘이 그걸 부르게)

개념 7 — 제약: 자유도를 줄이는 도구

망가지는 장면

삼각형을 다루는 객체를 만들었다.

세 변을 아무 값이나 넣을 수 있게 열어 뒀다.

누군가 (1, 1, 100)을 넣었다 — 세상에 없는 삼각형이다.

그 객체로 계산하니 결과가 엉망이 됐다.

비유 먼저

콘센트 구멍을 떠올리자.

구멍 모양이 정해져 있어, 안 맞는 플러그는 아예 안 꽂힌다.

그래서 엉뚱한 걸 꽂아 사고 날 일이 줄어든다.

제약은 이렇게 "안 되는 것을 못 하게 막아" 위험한 상태 자체를 없앤다.

비유 코드 위험
모양 맞는 콘센트(제약 있음) 변 넣을 때 삼각형 검사 잘못된 삼각형 못 만듦 — 안전
뚫린 구멍(제약 없음) 세 변을 아무 값이나 허용 (1,1,100) 같은 괴물 통과

한 문장 정의 — 제약은 부품이 가질 수 있는 상태나 할 수 있는 상호작용을 막는 규칙이며, 잘못된 상태를 처음부터 못 만들게 해 자유도를 줄인다.

커네빈으로 보면, 제약이 강할수록 원인과 결과가 단단히 이어져(명확 도메인 쪽) 예측이 쉽고, 제약이 없을수록 인과가 끊겨(혼돈 쪽) 복잡함이 커진다. 복잡한 상호작용을 길들이는 도구가 바로 제약이다.

예시 폭격

worked — 불변식으로 괴물 삼각형 막기:

public class Triangle // 유효한 삼각형이라는 제약을 객체 안에 가두는 예입니다.
{
    public dynamic SetEdges(dynamic a, dynamic b, dynamic c) // 이 메서드의 입력과 출력이 공개 약속입니다.
    {
        // 제약(불변식): 두 변의 합은 나머지 한 변보다 커야 한다
        if (a + b < c || a + c < b || b + c < a) // 조건이 참일 때만 아래 결정을 실행합니다.
        {
            throw new ArgumentException("유효하지 않은 삼각형"); // 잘못된 상태를 객체 안으로 들이지 않습니다.
        }
        // this.a = a; this.b = b; this.c = c; // 여러 값을 한 번에 넣는 C#에서는 개별 대입으로 풀어 쓰는 표현은 C#에서 개별 대입으로 풀어 씁니다.
}
}
// 이제 Triangle은 무조건 진짜 삼각형 — 자유도가 '유효한 상태'로 줄었다

잘못된 예 vs 올바른 예:

// 잘못된 예: 아무 값이나 허용 (제약 0)
var t = new Triangle(); t.A, t.B, t.C = 1, 1, 100; // 괴물 통과

// 올바른 예: 검사하는 입구로만 받게 (제약 도입)
var t = new Triangle(); t.SetEdges(3, 4, 5); // OK
t.SetEdges(1, 1, 100); // 막힘 — 예외 발생

미니 시나리오 — "이럴 때 이렇게":

// 상황: 입력값이 너무 자유로워 가끔 이상한 상태가 됨
// 안전판: "이 값은 이래야만 한다"는 규칙(불변식)을 입구에 건다
// 효과: 나쁜 상태를 만들 자유 자체가 사라짐

개념 8 — 결합 = 제약을 설계하는 일 (Repository 세 설계)

망가지는 장면

저장소(데이터를 넣고 빼는 부품)를 만들면서, 호출하는 쪽이 SQL을 직접 쓰게 열어 뒀다.

자유롭고 좋아 보였다.

그런데 누가 컬럼 이름을 바꾸자 여기저기 쿼리가 줄줄이 깨졌다.

나중에 DB를 바꾸려니 SQL을 거의 다 다시 써야 했다.

비유 먼저

식당 메뉴판을 떠올리자.

손님이 주방에 직접 들어가 재료를 뒤지게 하면 자유롭다 — 하지만 주방을 바꾸면 손님이 다 헤맨다.

손님에게 메뉴판만 주면 덜 자유롭지만 — 주방을 바꿔도 손님은 모른다. 안전하다.

결합을 설계한다는 건, "손님에게 얼마나 보여줄지"를 정하는 일이다.

비유 코드 위험
주방 개방(설계 A: SQL 직접) query("WHERE opened_on < ?") 컬럼명·DB 종류 다 노출 — 폭발
반쯤 가림(설계 B: Query 객체) query.where("OpenedOn", ...) DB 방언은 숨김, 컬럼명은 남음
메뉴판만(설계 C: 파인더 메서드) repo.created_before(date) 다 숨김 — 가장 안전, 덜 자유

한 문장 정의 — 결합을 설계한다는 것은 부품 사이에 어떤 지식을 공유하고 어떤 상호작용을 허용/금지할지 제약을 정하는 일이며, 잘 설계한 결합은 자유도를 줄여 복잡한 상호작용을 막는다.

세 설계를 자유도 순으로 늘어놓으면 이렇다.

  • 설계 A — SQL 직접: 호출자가 SQL을 그대로 쓴다. 가장 자유롭고, 가장 위험하다. DB 스키마·방언·인덱스를 다 알아야 한다.
  • 설계 B — Query 객체: SQL 대신 객체로 조건을 짠다. DB 방언은 숨겨지지만, 컬럼 이름은 여전히 새어 나온다.
  • 설계 C — 파인더 메서드: created_before 같이 미리 정해진 질문만 허용한다. 컬럼 이름도 안 보인다. 자유는 가장 적지만 가장 안전하다.

예시 폭격

worked — 설계 A의 위험(주방 개방):

// 호출자가 SQL을 직접 씀 → 컬럼 이름 opened_on 이 코드에 박힘
var cases = repo.Query("WHERE opened_on < ?", [three_months_ago]); // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
// 누가 컬럼명을 바꾸면? 이 줄이 조용히 깨진다. DB를 바꾸면? SQL을 다 다시 쓴다.

부분완성 — 설계 B는 무엇을 숨기고 무엇을 못 숨길까?

var q = new QueryObject().Where("OpenedOn", "<=", threeMonthsAgo); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
var cases = repo.Query(q); // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
// 숨겨진 것: ____ (정답: SQL 방언 — MySQL/Mongo 어느 쪽이든 내부가 변환)
// 아직 새는 것: ____ (정답: 컬럼/속성 이름 "OpenedOn")

독립적용 — 설계 C가 가장 안전한 이유와 대가를 한 줄씩:

var cases = repo.CreatedBefore(tenantId: 10, date: threeMonthsAgo); // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
// 안전한 이유: 미리 정한 질문만 받으니 컬럼명·SQL이 안 새고 자유도가 최소다.
// 대가:        새 질문이 필요하면 저장소에 메서드를 새로 추가해야 한다(유연성 ↓).

미니 시나리오 — "이럴 때 이렇게":

// 상황: 6개월 뒤 DB를 MySQL → 다른 DB로 옮길 예정
// 판단: 설계 A는 옮길 때 SQL을 다 다시 써야 함 → 비쌈
// 설계 C는 저장소 구현만 갈면 끝 → 이전 비용 최소
// 선택: 이전이 예정됐다면 자유를 조금 포기하고 C로 간다

단순 규칙

  • 복잡함을 만났으면 먼저 가른다 — 도메인 탓(필수적, 관리만)인가, 내 탓(우발적, 제거)인가.
  • 쪼갤지 합칠지는 크기가 아니라 상호작용으로 정한다. "함께 변하나 / 서로 얼마나 아나"가 기준이다.
  • 같은 규칙·같은 데이터를 두 곳에 두지 않는다 — 자유도가 올라가 어긋난다.
  • 부품을 연결할 땐 가장 적게 보여주는 쪽을 기본값으로 둔다. 보여줄수록 깨질 자리가 는다.

더 해보기

(검증 2026-05-26 / Tavily+Brave+SearXNG — 위 링크는 3장 딥다이브 노트에서 교차확인)



연습문제

  1. 설명. 복잡함은 어디서 오나의 핵심을 처음 듣는 사람에게 한 문장으로 설명하라.
  2. 구분. 두 개념(지역 복잡성, 전역 복잡성)을 실제 예시 하나로 구분하라.
  3. 적용. 내 프로젝트나 학습 노트에서 이 장의 개념을 적용해 작게 개선할 지점을 하나 고르라.

부록 A. 쉬운 용어 사전

한글 용어 원문 영문명 아주 쉬운 뜻 이 장에서 나온 위치
지역 복잡성 Local Complexity 한 부품이나 모듈 안에서 느끼는 이해 부담. 부록 B와 본문 예시
전역 복잡성 Global Complexity 여러 부품 사이 관계에서 생기는 이해 부담. 부록 B와 본문 예시
자유도 Degrees of Freedom 시스템 상태를 정하려고 따로 결정해야 하는 값의 수. 부록 B와 본문 예시
제약 Constraint 가능한 선택지를 줄여 시스템을 더 예측 가능하게 만드는 장치. 부록 B와 본문 예시

부록 B. 헷갈리는 개념 비교표

A B 구분 포인트
지역 복잡성 전역 복잡성 지역은 한 부품 안 부담, 전역은 부품 사이 관계 부담이다.
자유도 제약 자유도는 정할 것이 많은 상태, 제약은 선택지를 줄여 안정시키는 장치다.

부록 C. 더 읽을 자료

  • 이 장의 더 해보기 섹션 — 이미 모아 둔 공식 문서나 실습 링크가 있으면 여기서 먼저 확인한다.
  • 같은 책의 0장 한눈에 보기 — 용어가 막히면 0장의 용어집과 개념 척추로 돌아간다.
  • 원본 딥다이브판 같은 장 — 입문판을 읽고 큰 흐름이 잡힌 뒤 세부 논리를 더 깊게 확인한다.
  • 이 장의 flashcards.json — 읽은 직후 질문만 보고 답을 떠올리는 회상 연습에 쓴다.

부록 D. 연습문제 풀이

  1. 설명 예시. 복잡함은 어디서 오나는 변경이 어디로 번지는지 보고, 필요한 연결과 줄여야 할 연결을 구분하게 해 주는 장이다. 중요한 것은 용어를 외우는 것이 아니라, 이 개념이 어떤 입력·부품·결정에 영향을 주는지 말로 풀어 보는 것이다.
  2. 구분 예시. 두 개념(지역 복잡성, 전역 복잡성)의 차이는 이렇게 잡으면 된다. 지역은 한 부품 안 부담, 전역은 부품 사이 관계 부담이다. 실제 사례를 볼 때는 목적, 입력, 실패했을 때의 증상을 따로 적어 보면 헷갈리지 않는다.
  3. 적용 예시. 가장 작은 개선부터 고른다. 예를 들어 이름을 더 분명히 하거나, 평가 기준을 한 줄 추가하거나, 직접 알 필요 없는 내부 정보를 감추는 식으로 시작한다. 한 번에 크게 갈아엎는 것보다 작은 변경 하나를 확인하며 진행하는 쪽이 입문 단계에 맞다.

다음 장 예고

다음 장에서는 이 복잡함의 반대편 — 시스템을 어떻게 깔끔한 조각으로 나누는가를 본다.

지금은 "복잡함은 개수가 아니라 얽힘에서 오고, 제약으로 자유도를 줄인다"만 들고 가면 충분하다.

난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플